package joe;

import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.SymbolLookup;
import java.lang.invoke.MethodHandle;
import java.util.ArrayList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.tinylog.Logger;
import static java.lang.foreign.ValueLayout.JAVA_FLOAT;
import static java.lang.foreign.ValueLayout.JAVA_INT;
import static java.lang.foreign.ValueLayout.JAVA_LONG;

public class AudioMini implements Audio {
	private static final @NotNull MethodHandle mhMaEngineInit;
	private static final @NotNull MethodHandle mhMaEngineDestroy;
	private static final @NotNull MethodHandle mhMaEngineLoadSound;
	private static final @NotNull MethodHandle mhMaSoundDestroy;
	private static final @NotNull MethodHandle mhMaSoundPlay;
	private static final @NotNull MethodHandle mhMaSoundStop;
	private static final @NotNull MethodHandle mhMaSoundPause;
	private static final @NotNull MethodHandle mhMaSoundResume;
	private static final @NotNull MethodHandle mhMaSoundSetVolume;
	private static final @NotNull MethodHandle mhMaSoundSetPan;
	private static final @NotNull MethodHandle mhMaSoundSetPitch;
	private static final @NotNull MethodHandle mhMaSoundSetPos;
	private static final @NotNull MethodHandle mhMaSoundSetRange;
	private static final @NotNull MethodHandle mhMaSoundSetLoop;
	private static final @NotNull MethodHandle mhMaSoundGetTotalFrames;
	private static final @NotNull MethodHandle mhMaSoundGetSampleRate;
	private static final @NotNull MethodHandle mhMaSoundGetPos;
	private static final @NotNull MethodHandle mhMaDecodeOpus;

	static {
		var libMiniAudio = SymbolLookup.libraryLookup(System.mapLibraryName("miniaudio"), Arena.global());
		var linker = Linker.nativeLinker();
		// int maEngineInit(ma_engine** ppEngine);
		mhMaEngineInit = linker.downcallHandle(libMiniAudio.find("maEngineInit").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG));
		// void maEngineDestroy(ma_engine* pEngine);
		mhMaEngineDestroy = linker.downcallHandle(libMiniAudio.find("maEngineDestroy").orElseThrow(),
				FunctionDescriptor.ofVoid(JAVA_LONG));
		// int maEngineLoadSound(ma_engine* pEngine, const void* pData, uint64_t dataSize, Sound** ppSound);
		mhMaEngineLoadSound = linker.downcallHandle(libMiniAudio.find("maEngineLoadSound").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG, JAVA_LONG, JAVA_LONG), Linker.Option.critical(false));
		// int maSoundDestroy(Sound* pSound);
		mhMaSoundDestroy = linker.downcallHandle(libMiniAudio.find("maSoundDestroy").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG), Linker.Option.critical(false));
		// int maSoundPlay(Sound* pSound);
		mhMaSoundPlay = linker.downcallHandle(libMiniAudio.find("maSoundPlay").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG), Linker.Option.critical(false));
		// int maSoundStop(Sound* pSound);
		mhMaSoundStop = linker.downcallHandle(libMiniAudio.find("maSoundStop").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG), Linker.Option.critical(false));
		// int maSoundPause(Sound* pSound);
		mhMaSoundPause = linker.downcallHandle(libMiniAudio.find("maSoundPause").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG), Linker.Option.critical(false));
		// int maSoundResume(Sound* pSound);
		mhMaSoundResume = linker.downcallHandle(libMiniAudio.find("maSoundResume").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG), Linker.Option.critical(false));
		// int maSoundSetVolume(Sound* pSound, float volume);
		mhMaSoundSetVolume = linker.downcallHandle(libMiniAudio.find("maSoundSetVolume").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_FLOAT), Linker.Option.critical(false));
		// int maSoundSetPan(Sound* pSound, float pan);
		mhMaSoundSetPan = linker.downcallHandle(libMiniAudio.find("maSoundSetPan").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_FLOAT), Linker.Option.critical(false));
		// int maSoundSetPitch(Sound* pSound, float pitch);
		mhMaSoundSetPitch = linker.downcallHandle(libMiniAudio.find("maSoundSetPitch").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_FLOAT), Linker.Option.critical(false));
		// int maSoundSetPos(Sound* pSound, uint64_t pos);
		mhMaSoundSetPos = linker.downcallHandle(libMiniAudio.find("maSoundSetPos").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG), Linker.Option.critical(false));
		// int maSoundSetRange(Sound* pSound, uint64_t beginPos, uint64_t endPos);
		mhMaSoundSetRange = linker.downcallHandle(libMiniAudio.find("maSoundSetRange").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG, JAVA_LONG), Linker.Option.critical(false));
		// int maSoundSetLoop(Sound* pSound, int loop);
		mhMaSoundSetLoop = linker.downcallHandle(libMiniAudio.find("maSoundSetLoop").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_INT), Linker.Option.critical(false));
		// int maSoundGetTotalFrames(Sound* pSound, uint64_t* pFrames);
		mhMaSoundGetTotalFrames = linker.downcallHandle(libMiniAudio.find("maSoundGetTotalFrames").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG), Linker.Option.critical(false));
		// int maSoundGetSampleRate(Sound* pSound, int* pSampleRate);
		mhMaSoundGetSampleRate = linker.downcallHandle(libMiniAudio.find("maSoundGetSampleRate").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG), Linker.Option.critical(false));
		// int maSoundGetPos(Sound* pSound, uint64_t* pPos);
		mhMaSoundGetPos = linker.downcallHandle(libMiniAudio.find("maSoundGetPos").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG), Linker.Option.critical(false));
		// int maDecodeOpus(int sampleRate, int channel, ma_uint64 frames, const void* pSrcData, uint64_t srcDataSize, void* pDstData, uint64_t dstDataSize);
		mhMaDecodeOpus = linker.downcallHandle(libMiniAudio.find("maDecodeOpus").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, JAVA_INT, JAVA_INT, JAVA_LONG, JAVA_LONG, JAVA_LONG, JAVA_LONG, JAVA_LONG));
	}

	// MMMmmCnn
	public static @NotNull String getOpusMode(int payloadByte0) {
		if (payloadByte0 < 0)
			return "CELT_ONLY";
		if ((payloadByte0 & 0x60) == 0x60)
			return "HYBRID";
		return "SILK_ONLY";
	}

	public static int getOpusChannels(int payloadByte0) {
		return ((payloadByte0 >> 2) & 1) + 1;
	}

	/**
	 * @return 4k, 6k, 8k, 12k, 20k (Hz bandpass)
	 */
	public static int getOpusBandwidth(int payloadByte0) {
		if (payloadByte0 < 0) { // CELT_ONLY
			return switch ((payloadByte0 >> 5) & 3) {
				case 0 -> 4000;
				case 1 -> 8000;
				case 2 -> 12000;
				default -> 20000;
			};
		}
		if ((payloadByte0 & 0x60) == 0x60) // HYBRID
			return (payloadByte0 & 0x10) != 0 ? 20000 : 12000;
		return switch ((payloadByte0 >> 5) & 3) { // SILK_ONLY
			case 0 -> 4000;
			case 1 -> 6000;
			case 2 -> 8000;
			default -> 12000;
		};
	}

	/**
	 * @return 2(2.5), 5, 10, 20, 40, 60, 80, 100, 120
	 */
	public static int getOpusPayloadMs(int payloadByte0, int payloadByte1) {
		int n = switch (payloadByte0 & 3) {
			case 0 -> 1; // one frame
			case 1, 2 -> 2; // two cbr/vbr frames
			default -> payloadByte1 & 0x3f; // 0~63, multiple cbr/vbr(payloadByte1<0) frames
		};
		int m = (payloadByte0 >> 3) & 3;
		if (payloadByte0 < 0) // CELT_ONLY
			m = (1000 << m) / 400; // 2.5,5,10,20
		else if ((payloadByte0 & 0x60) == 0x60) // HYBRID
			m = 10 << (m & 1);
		else // SILK_ONLY
			m = m == 3 ? 60 : 10 << m;
		return n * m;
	}

	public static int decodeOpusToWaveFile(long srcDataPtr, long srcDataSize, long dstDataOutPtr, long dstDataSizePtr) {
		if (srcDataSize < 8)
			throw new IllegalArgumentException("srcDataSize=" + srcDataSize + " < 8");
		if (dstDataOutPtr == 0)
			throw new IllegalArgumentException("dstDataOutPtr=0");
		if (dstDataSizePtr == 0)
			throw new IllegalArgumentException("dstDataSizePtr=0");
		final int magic = UnsafeUtil.getInt(srcDataPtr);
		final int channels = switch (magic >>> 24) {
			case '1' -> 1;
			case '2' -> 2;
			default -> throw new IllegalArgumentException("unknown profile");
		};
		final var totalFrames = UnsafeUtil.getInt(srcDataPtr + 4) & 0xffff_ffffL;
		final var packetCount = totalFrames > 0 ? (312/*TRIM_BEGIN_FRAMES*/ + totalFrames + 5760 - 1) / 5760 : 0;
		srcDataPtr += 8;
		srcDataSize -= 8;
		var dstDataAllocSize = 0x2c + packetCount * 5760 * channels * 2;
		var dstDataPtr = UnsafeUtil.allocateMemory(dstDataAllocSize);
		try {
			int r = (int)mhMaDecodeOpus.invokeExact(48000, channels, totalFrames, srcDataPtr, srcDataSize, dstDataPtr, dstDataAllocSize);
			UnsafeUtil.putLong(dstDataOutPtr, dstDataPtr);
			UnsafeUtil.putLong(dstDataSizePtr, 0x2c + totalFrames * channels * 2);
			return r;
		} catch (Throwable e) { // rethrow
			UnsafeUtil.freeMemory(dstDataPtr);
			throw UnsafeUtil.forceThrow(e);
		}
	}

	private long enginePtr;
	private final ArrayList<@NotNull SoundMini> updaters = new ArrayList<>();

	@Override
	public void init() {
		if (enginePtr != 0)
			throw new IllegalStateException("already inited");
		try {
			var tmpBuf = Engine.current().getTmpBuf();
			int err = (int)mhMaEngineInit.invokeExact(tmpBuf.ptr());
			if (err != 0)
				throw new IllegalStateException("maEngineInit failed: " + err);
			enginePtr = tmpBuf.getLong();
		} catch (Throwable e) { // rethrow
			destroy();
			UnsafeUtil.forceThrow(e);
		}
	}

	@Override
	public void destroy() {
		updaters.clear();
		if (enginePtr != 0) {
			try {
				mhMaEngineDestroy.invokeExact(enginePtr);
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
			enginePtr = 0;
		}
	}

	void addUpdater(@NotNull SoundMini sound) {
		updaters.add(sound);
	}

	@Override
	public void poll() {
		if (!updaters.isEmpty()) {
			var curNs = System.nanoTime();
			updaters.removeIf(sound -> !sound.onUpdate(curNs));
		}
	}

	@Override
	public @NotNull Sound loadSound(long dataPtr, long dataSize, boolean stream) {
		if (enginePtr == 0)
			throw new IllegalStateException("destroyed");
		var tmpBuf = Engine.current().getTmpBuf();
		var ownPtr = false;
		var soundPtr = 0L;
		try {
			if (!stream && dataPtr != 0 && dataSize >= 8 && (UnsafeUtil.getInt(dataPtr) & 0xffffff) == 0x41504f) { // "OPA"
				int err = decodeOpusToWaveFile(dataPtr, dataSize, tmpBuf.ptr(), tmpBuf.ptr(8));
				if (err != 0)
					throw new IllegalStateException("maDecodeOpus failed: " + err);
				dataPtr = tmpBuf.getLong();
				dataSize = tmpBuf.getLong(1);
				ownPtr = true;
			}
			int err = (int)mhMaEngineLoadSound.invokeExact(enginePtr, dataPtr, dataSize, tmpBuf.ptr());
			if (err != 0)
				throw new IllegalStateException("maEngineLoadSound failed: " + err);
			soundPtr = tmpBuf.getLong();
			return new SoundMini(soundPtr, dataPtr, dataSize, ownPtr ? new long[1] : null, stream);
		} catch (Throwable e) { // rethrow
			try {
				try {
					if (soundPtr != 0) {
						int err = (int)mhMaSoundDestroy.invokeExact(soundPtr);
						if (err != 0)
							throw new IllegalStateException("maSoundDestroy failed: " + err);
					}
				} finally {
					if (ownPtr)
						UnsafeUtil.freeMemory(dataPtr);
				}
			} catch (Throwable ex) { // rethrow
				UnsafeUtil.forceThrow(ex);
			}
			throw UnsafeUtil.forceThrow(e);
		}
	}

	private static final class BoolWrap {
		boolean b;
	}

	private static final class SoundMini implements Sound {
		private final long soundPtr;
		private final long dataPtr, dataSize;
		private final long @Nullable [] ownRefCount;
		private final boolean stream;
		private long totalFrames = -1, beginPos, endPos = -1;
		private float curVolume = 1, toVolume = 1, pan, pitch = 1;
		private float fadeSpeed; // volume / ns
		private long lastUpdateNs;
		private byte updateFinishAction; // 1:pause; 2:stop; 3:destroy
		private boolean loop;
		private boolean updating;
		private boolean destroyWhenPlayEnd;
		private final BoolWrap destroyed = new BoolWrap();

		private SoundMini(long soundPtr, long dataPtr, long dataSize, long @Nullable [] ownRefCount, boolean stream) {
			this.soundPtr = soundPtr;
			this.dataPtr = dataPtr;
			this.dataSize = dataSize;
			this.ownRefCount = ownRefCount;
			this.stream = stream;
			if (ownRefCount != null)
				ownRefCount[0]++;
			var destroyed0 = destroyed;
			new Cleaner.Node(this, Engine.current().getCleaner()) {
				@Override
				protected void clean() {
					destroy(soundPtr, dataPtr, ownRefCount, destroyed0);
				}
			};
		}

		boolean onUpdate(long curNs) {
			try {
				if (destroyed.b) {
					updating = false;
					return false;
				}
				if (destroyWhenPlayEnd && getPos() >= getEndPos()) {
					updating = false;
					destroy(false);
					return false;
				}
				var ns = curNs - lastUpdateNs;
				lastUpdateNs = curNs;
				var volume = curVolume;
				if (volume != toVolume) {
					var delta = fadeSpeed > 0 ? fadeSpeed * ns : 1;
					if (volume < toVolume)
						volume = Math.min(volume + delta, toVolume);
					else
						volume = Math.max(volume - delta, toVolume);
					setCurVolume(volume);
					if (volume != toVolume)
						return true;
				}
				updating = false;
				switch (updateFinishAction) {
				case 1:
					pause();
					break;
				case 2:
					stop();
					break;
				case 3:
					destroy(false);
					break;
				}
			} catch (Throwable e) { // logger.error
				Logger.error(e, "AudioMini: onUpdate exception");
			}
			return false;
		}

		@Override
		public void setPos(long frame) {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			try {
				int err = (int)mhMaSoundSetPos.invokeExact(soundPtr, frame);
				if (err != 0)
					throw new IllegalStateException("maSoundSetPos failed: " + err);
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
		}

		@Override
		public void setRange(long beginFrame, long endFrame) {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			if (beginFrame < 0)
				beginFrame = 0;
			if (endFrame < 0)
				endFrame = getTotalFrames();
			if (beginFrame > endFrame) {
				var t = beginFrame;
				beginFrame = endFrame;
				endFrame = t;
			}
			try {
				int err = (int)mhMaSoundSetRange.invokeExact(soundPtr, beginFrame, endFrame);
				if (err != 0)
					throw new IllegalStateException("maSoundSetRange failed: " + err);
				beginPos = beginFrame;
				endPos = endFrame;
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
		}

		@Override
		public void setLoop(boolean loop) {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			if (this.loop == loop || destroyWhenPlayEnd)
				return;
			try {
				int err = (int)mhMaSoundSetLoop.invokeExact(soundPtr, loop ? 1 : 0);
				if (err != 0)
					throw new IllegalStateException("maSoundSetLoop failed: " + err);
				this.loop = loop;
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
		}

		private void setCurVolume(float volume) {
			try {
				int err = (int)mhMaSoundSetVolume.invokeExact(soundPtr, volume);
				if (err != 0)
					throw new IllegalStateException("maSoundSetVolume failed: " + err);
				this.curVolume = volume;
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
		}

		@Override
		public void setVolume(float volume) {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			this.toVolume = volume;
			if (fadeSpeed == 0)
				setCurVolume(volume);
			else
				tryAddVolumeUpdater();
		}

		private void tryAddVolumeUpdater() {
			if (!updating && (curVolume != toVolume || destroyWhenPlayEnd)) {
				lastUpdateNs = System.nanoTime();
				updating = true;
				((AudioMini)Engine.current().getAudio()).addUpdater(this);
			}
		}

		@Override
		public void setPan(float pan) {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			try {
				int err = (int)mhMaSoundSetPan.invokeExact(soundPtr, pan);
				if (err != 0)
					throw new IllegalStateException("maSoundSetPan failed: " + err);
				this.pan = pan;
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
		}

		@Override
		public void setPitch(float pitch) {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			try {
				int err = (int)mhMaSoundSetPitch.invokeExact(soundPtr, pitch);
				if (err != 0)
					throw new IllegalStateException("maSoundSetPitch failed: " + err);
				this.pitch = pitch;
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
		}

		@Override
		public long getTotalFrames() {
			if (totalFrames < 0) {
				if (destroyed.b)
					throw new IllegalStateException("destroyed");
				var tmpBuf = Engine.current().getTmpBuf();
				try {
					int err = (int)mhMaSoundGetTotalFrames.invokeExact(soundPtr, tmpBuf.ptr());
					if (err != 0)
						throw new IllegalStateException("maSoundGetTotalFrames failed: " + err);
				} catch (Throwable e) { // rethrow
					UnsafeUtil.forceThrow(e);
				}
				totalFrames = tmpBuf.getLong();
			}
			return totalFrames;
		}

		@Override
		public int getSampleRate() {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			var tmpBuf = Engine.current().getTmpBuf();
			try {
				int err = (int)mhMaSoundGetSampleRate.invokeExact(soundPtr, tmpBuf.ptr());
				if (err != 0)
					throw new IllegalStateException("maSoundGetSampleRate failed: " + err);
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
			return tmpBuf.getInt();
		}

		@Override
		public long getPos() {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			var tmpBuf = Engine.current().getTmpBuf();
			try {
				int err = (int)mhMaSoundGetPos.invokeExact(soundPtr, tmpBuf.ptr());
				if (err != 0)
					throw new IllegalStateException("maSoundGetPos failed: " + err);
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
			return tmpBuf.getLong();
		}

		@Override
		public long getBeginPos() {
			return beginPos;
		}

		@Override
		public long getEndPos() {
			if (endPos < 0)
				endPos = getTotalFrames();
			return endPos;
		}

		@Override
		public boolean isLoop() {
			return loop;
		}

		@Override
		public float getCurVolume() {
			return curVolume;
		}

		@Override
		public float getVolume() {
			return toVolume;
		}

		@Override
		public float getPan() {
			return pan;
		}

		@Override
		public float getPitch() {
			return pitch;
		}

		@Override
		public void setFadeSpeed(float ms) {
			fadeSpeed = ms > 0 ? 0.000_001f / ms : 0;
		}

		@Override
		public void play() {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			try {
				int err = (int)mhMaSoundPlay.invokeExact(soundPtr);
				if (err != 0)
					throw new IllegalStateException("maSoundPlay failed: " + err);
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
		}

		@Override
		public void pause() {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			if (updating) {
				updateFinishAction = 1;
				return;
			}
			try {
				int err = (int)mhMaSoundPause.invokeExact(soundPtr);
				if (err != 0)
					throw new IllegalStateException("maSoundPause failed: " + err);
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
		}

		@Override
		public void resume() {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			try {
				int err = (int)mhMaSoundResume.invokeExact(soundPtr);
				if (err != 0)
					throw new IllegalStateException("maSoundResume failed: " + err);
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
		}

		@Override
		public void stop() {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			if (updating) {
				updateFinishAction = 2;
				return;
			}
			try {
				int err = (int)mhMaSoundStop.invokeExact(soundPtr);
				if (err != 0)
					throw new IllegalStateException("maSoundStop failed: " + err);
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
		}

		@Override
		public void destroy(boolean whenPlayEnd) {
			if (destroyed.b)
				return;
			if (whenPlayEnd) {
				if (!destroyWhenPlayEnd) {
					setLoop(false);
					destroyWhenPlayEnd = true;
					tryAddVolumeUpdater();
				}
				return;
			}
			if (updating) {
				updateFinishAction = 3;
				return;
			}
			destroy(soundPtr, dataPtr, ownRefCount, destroyed);
		}

		private static void destroy(long soundPtr, long dataPtr, long @Nullable [] ownRefCount, @NotNull BoolWrap destroyed) {
			if (destroyed.b)
				return;
			destroyed.b = true;
			try {
				int err = (int)mhMaSoundDestroy.invokeExact(soundPtr);
				if (ownRefCount != null && --ownRefCount[0] == 0)
					UnsafeUtil.freeMemory(dataPtr);
				if (err != 0)
					throw new IllegalStateException("maSoundDestroy failed: " + err);
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
		}

		@SuppressWarnings("MethodDoesntCallSuperMethod")
		@Override
		public @NotNull Sound clone() {
			if (destroyed.b)
				throw new IllegalStateException("destroyed");
			var engine = Engine.current();
			var tmpBuf = engine.getTmpBuf();
			try {
				int err = (int)mhMaEngineLoadSound.invokeExact(engine.getAudio(), dataPtr, dataSize, tmpBuf.ptr());
				if (err != 0)
					throw new IllegalStateException("maEngineLoadSound failed: " + err);
			} catch (Throwable e) { // rethrow
				UnsafeUtil.forceThrow(e);
			}
			var sound = new SoundMini(tmpBuf.getLong(), dataPtr, dataSize, ownRefCount, stream);
			try {
				sound.totalFrames = totalFrames;
				if (beginPos != 0 || endPos != -1)
					sound.setRange(beginPos, endPos);
				if (toVolume != 1)
					sound.setVolume(toVolume);
				if (pan != 0)
					sound.setPan(pan);
				if (pitch != 1)
					sound.setPitch(pitch);
				if (loop)
					sound.setLoop(true);
				return sound;
			} catch (Throwable e) { // rethrow
				sound.destroy(false);
				throw e;
			}
		}
	}
}
